package com.xiaoye.poitest;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;

/**
 * Class description
 *   将阿拉伯数字转为人民币大写，转换规则如下：
 *   1、采用求商取余方法获取每一位的阿拉伯数字，余数作为下次计算的参数进入下一次递归
 *   2、人民币大写只包含壹、贰、叁、肆、伍、陆、柒、捌、玖、拾、佰、仟、万、亿、元、角、分、零、整(正)等字样
 *   3、如果人民币不带小数位，应当在后边拼接"元整"
 *   4、阿拉伯数字如果有连续多个0出现的，转换为人民币大写应当只包含1个"零"，例如：10001，转换为人民币大写为：壹万零壹元整
 * @author xiaoqian
 * <p>
 * <p> Modification History:</p>
 * <p> Date              Author      Description </p>
 * <p>------------------------------------------------------------------</p>
 * <p>2021/1/12 8:53 AM    韩洪千                新建</p>
 */
public class XyRmbTransLowerOrUpper {
    //阿拉伯数字与中文金额大写的映射关系，阿拉伯数字作为key值，映射值为中文大写
    private static final Map<String,String>  digitMap = new HashMap<>();
    //阿拉伯数字所在位置（从右向左）的单位，例如：1234，1：代表的是1仟，所以4位，单位仟
    private static final Map<Integer,String> digitBit = new HashMap<>();
    //阿拉伯数字所在位置（从右向左）的单位，例如：1234，1：代表的是1仟，所以4位，单位仟
    private static final Map<Integer,String> decimalBit = new HashMap<>();
    //人民币大写单位，包含亿、万、元，个位元在此省略
    private static final String  UNIT_YI = "亿";
    private static final String  UNIT_WAN = "万";
    //将阿拉伯金额数字拆分为3组，每组4位，所以此程序可以转换的最大金额为999999999999.99元
    private static final int MAX_LENGTH = 3;
    //每组展示的阿拉伯数字最大个数
    private static final double MAX_COUNTS = 4;
    //整数位最大位数到千亿级
    private static final  int MAX_DIGIT_LENGTH = 12;
    //中文大写与阿拉伯数字的映射，用来大写转小写的实现
    private static final Map<String,Integer>   adverseDigitMap = new HashMap<>();
    //存放中文金额单位的 10进制表示，用来大写转小写实现
    private static final Map<String,String>   adverseDigitMapUnit = new HashMap<>();

    static {
        //阿拉伯数字与汉字映射
        digitMap.put("0","零");
        digitMap.put("1","壹");
        digitMap.put("2","贰");
        digitMap.put("3","叁");
        digitMap.put("4","肆");
        digitMap.put("5","伍");
        digitMap.put("6","陆");
        digitMap.put("7","柒");
        digitMap.put("8","捌");
        digitMap.put("9","玖");

        //整数点位
        digitBit.put(1,"");
        digitBit.put(2,"拾");
        digitBit.put(3,"佰");
        digitBit.put(4,"仟");

        //小数点位
        decimalBit.put(0,"角");
        decimalBit.put(1,"分");

        adverseDigitMap.put("零",0);
        adverseDigitMap.put("壹",1);
        adverseDigitMap.put("贰",2);
        adverseDigitMap.put("叁",3);
        adverseDigitMap.put("肆",4);
        adverseDigitMap.put("伍",5);
        adverseDigitMap.put("陆",6);
        adverseDigitMap.put("柒",7);
        adverseDigitMap.put("捌",8);
        adverseDigitMap.put("玖",9);

        adverseDigitMapUnit.put("亿","100000000");
        adverseDigitMapUnit.put("万","10000");
        adverseDigitMapUnit.put("仟","1000");
        adverseDigitMapUnit.put("佰","100");
        adverseDigitMapUnit.put("拾","10");
        adverseDigitMapUnit.put("元","1");
        adverseDigitMapUnit.put("角","0.1");
        adverseDigitMapUnit.put("分","0.01");
    }

    /**
     *
     * @param amount  金额
     * @param s  拼接字符串的缓冲区，用于接收最终的结果
     * @param count
     *    算法采用分组方式，每一组为4位阿拉伯数字，共计4位，所以取得每一位的数字需要进行count次递归求商取余
     * @return
     */
    private static String countIntegerChineseCharacter(String amount,StringBuffer s,int count){
        int j = count;
        //求商取余的次数，如果=0说明已经计算完成，退出递归调用
        if(j == 0)
            return "";
        //计算除数，10进制，为10的n次幂
        int divide = (int) Math.pow(10,j-1);
        //取商，获取当前位置的阿拉伯数字
        int sData = Integer.valueOf(amount) / divide;
        //如果有连续的0出现，只允许出现一次0
        if((s.length() > 1) && (s.charAt(s.length()-1) == '零')){
            if(sData > 0){
                s.append(digitMap.get(String.valueOf(sData)));
            }
        }else {
            s.append(digitMap.get(String.valueOf(sData)));
        }

        if(sData > 0){
            s.append(digitBit.get(j));
        }
        //求模，作为下次递归的计算因子
        int ssData = Integer.valueOf(amount) % divide;
        if(ssData == 0){
            return s.toString();
        }else{
            //递归调用
            return countIntegerChineseCharacter(String.valueOf(ssData),s,--j);
        }
    }

    /**
     * 获得金额中文大写
     * @param amount 待要转换的金额
     * @return
     */
    private static String getIntegerChineseAmount(String amount){
        StringBuffer buffer = new StringBuffer();
        //如果字符串大于1将字符串分组，每一组4个数字,从高位到低位进行分组
        int len = (int) Math.ceil((double)amount.length()/MAX_COUNTS);
        String[] groups = new String[MAX_LENGTH];
        if(len <= 1){
            countIntegerChineseCharacter(amount,buffer,amount.length());
        }else{
            //从低位向高位截取字符串，每4位数字为一组
            for(int i = 0;i < MAX_LENGTH;i++){
                final int start = amount.length() - ((i+1) << 2);
                final int end = start + (int) MAX_COUNTS;
                if(end >= 1){
                    String sAmount = amount.substring((start < 0) ? 0 : start,end);
                    groups[MAX_LENGTH -i -1] = sAmount;
                }
            }
            //对每一组数组进行大写转换
            for(int i = 0;i < groups.length;i++){
                String thisVal = groups[i];
                //如果该分组没有数据继续从其他分组中查找数据
                if(thisVal == null)
                    continue;
                //初始化对数据短除的次数，默认为4次
                int counts = (int) MAX_COUNTS;
                //如果是第1个分组取该数据的实际长度，否则会出现这样的情况例如6，在数据转换的算法中会处理成：零零零陆
                if(i == 0)
                    counts = thisVal.length();
                if((groups[0] == null) && (i == 1))
                    counts = thisVal.length();
                if(!"0000".equals(groups[i])){
                    //
                    String sMounts = countIntegerChineseCharacter(thisVal,buffer,counts);
                    if(i == 0)
                        buffer.append(UNIT_YI);
                    if(i == 1)
                        buffer.append(UNIT_WAN);
                }
            }
        }
        return buffer.toString();
    }

    /**
     * 获得小数位 精确到分  例如.56  伍角陆分，精确到分，超出范围舍去最后一位
     * 精确到分
     * @param amount
     * @return
     */
    private static String getDecimalChineseAmount(String amount){
        StringBuffer buffer = new StringBuffer();
        if("".equals(amount))
            return "";
        char[] digits = amount.toCharArray();
        for(int i = 0;i < digits.length;i++){
            //如果小数点超出两位，后边的位数不在进行处理，只精确到分
            if(i > 1)
                break;
            String val = String.valueOf(digits[i]);
            //小数位省略零
            if(!"0".equals(val)){
                buffer.append(digitMap.get(val));
                buffer.append(decimalBit.get(i));
            }
        }
        return buffer.toString();
    }

    /**
     * 金额转大写
     * @param amount  金额
     * @return
     */
    public static String transAmountToUpper(String amount){
        StringBuffer s = new StringBuffer();
        //初始化整数位
        String integerAmount = "";
        //初始化小数位
        String decimalAmount = "";
        String[] leftAndRight = amount.split("[.]");
        integerAmount = leftAndRight[0];
        if(integerAmount.length() > MAX_DIGIT_LENGTH)
            throw new IllegalArgumentException("只允许转换的最大金额为：999999999999.99元");
        //小数位
        decimalAmount = (leftAndRight.length > 1) ? leftAndRight[1]:decimalAmount;
        //整数为0不进行拼接 例如0.56元，转换后显示为陆分
        if(!"0".equals(integerAmount)){
            //拼接整数位
            s.append(getIntegerChineseAmount(integerAmount));
            //如果没有小数位显示元整例如 12 显示 壹拾贰元整
            s.append((leftAndRight.length > 1) ? "元":"元整");
        }
        //拼接小数位
        s.append(getDecimalChineseAmount(decimalAmount));
        return s.toString();
    }

    /**
     * 实现人民币大写转成阿拉伯数字（允许转换的最大金额为千亿级，超过千亿抛出异常进行处理）
     * 具体思路，将人民币数字转换为阿拉伯数字，并将单位转换为10进制表现形式
     * 表现为：a1*10^n + a2*10^(n-1)+……    最终得到人民币阿拉伯数字的表现形式
     * 将字符串进行分组，分别以亿、万  进行分组，分别对每组进行计算求和并乘以系数便得到每组的数值，将每组的数值相加
     * 1、将字符串进行拆分，并将代表0-9的大写数字与单位分开，根据大写金额规律，每一个阿拉伯数字后边紧跟的是单位（零已经舍去）
     * 2、拆分后将代表阿拉伯的数字作为k，代表金额的单位作为value，放入集合，这里请注意，因为数字是允许重复的，所以不能放入HashMap
     *    而要放入允许键重复的映射集合，这里通过查阅java api   采用IdentityHashMap 映射
     * 3、遍历集合，计算每一个键值对，键值对相乘，便得到一个计算因子
     * 4、将所有键值对的乘积相加边得到最终的代表阿拉伯数字的小写金额，相比小写转大写，大写转小野用容易
     * 示例：壹拾万零壹佰玖拾玖元壹分
     * @param amount
     * @return
     */
    public static String transAmountToLower(String amount){
        String nextAmount = amount;
        if(amount == null || amount.length() == 0)
            return "";
        //初始化最终结果
        BigDecimal res = new BigDecimal(0);
        //如果已经超过亿元,后边两段代码可优化，减少重复代码
        if(nextAmount.indexOf("亿") >= 1){
            int end = nextAmount.indexOf("亿");
            //获得万出现第一次位置
            int indexW = nextAmount.indexOf("万");
            //indexW 小于 end  说明此金额超过千亿，抛出异常退出程序
            if(indexW < end){
                throw new IllegalArgumentException("允许转换的最大金额为千亿级，已超出范围！");
            }
            int ratio = 100000000;
            //当前分组的金额
            String groupStr = nextAmount.substring(0,end);
            //获得当前分组金额并与初始值相加
            res = res.add(getGroupSum(groupStr,ratio));
            //获取亿单位后边的字符串
            nextAmount = nextAmount.substring(end + 1,nextAmount.length());
        }
        if(nextAmount.indexOf("万") >= 1){
            int end = nextAmount.indexOf("万");
            int ratio = 10000;
            //当前分组的金额
            String groupStr = nextAmount.substring(0,end);
            //获得当前分组金额并与初始值相加
            res = res.add(getGroupSum(groupStr,ratio));
            //获取亿单位后边的字符串
            nextAmount = nextAmount.substring(end + 1,nextAmount.length());
        }
        res = res.add(getGroupSum(nextAmount,1));
        return res.toString();
    }

    /**
     * 得到每一组的数值
     * @param amount
     * @param ratio
     * @return
     */
    private static BigDecimal getGroupSum(String amount,int ratio){
        //把零舍去并转换为字符数组，在进行阿拉伯数字计算的时候不影响计算结果
        char[] chars = amount.replaceAll("零","").toCharArray();
        //这里采用IdentityHashMap，此集合允许重复，而hashmap不允许重复，不符合要求
        Map<String,String> digitUnit = new IdentityHashMap<>();
        for(int i = 0;i < chars.length;i++){
            String key = String.valueOf(chars[i]);
            //如果当前不是数字继续往后查找
            if(adverseDigitMap.get(key) == null)
                continue;
            //接下来查找数字代表的10进制单位，例如：拾万
            StringBuffer valbuffer = new StringBuffer();
            for(int j = i+1 ;j < chars.length;j++){
                String val = String.valueOf(chars[j]);
                String unit = adverseDigitMapUnit.get(val);
                //这个条件有BUG  应该继续往后找，如果存在一个比当前位置大的位，那么拼接
                //例如：壹仟零壹万
                if(unit == null)
                    break;
                valbuffer.append(val);
            }
            digitUnit.put(key,valbuffer.toString());
        }
        return countCountForLower(digitUnit,ratio);
    }

    /**
     *由大写金额计算成阿拉伯数字金额
     * sum（k*v）
     * @param map
     * @param ratio
     * @return
     */
    private static BigDecimal countCountForLower(Map<String,String> map,int ratio){
        final BigDecimal[] sum = {new BigDecimal(0)};
        //遍历集合
        map.forEach((k,v)->{
            //初始化K,V的计算器
            BigDecimal vDeciml = new BigDecimal(1);
            char[] chars = v.toCharArray();
            //因为单位可能是连续的，因此需要拆分在相乘  例如：拾万
            for(char c:chars){
                //计算连续单位的乘积
                vDeciml = vDeciml.multiply(new BigDecimal(adverseDigitMapUnit.get(String.valueOf(c))));
            }
            //计算k * v
            vDeciml = vDeciml.multiply(new BigDecimal(adverseDigitMap.get(k)));
            //求和
            sum[0] = sum[0].add(vDeciml);
        });
        return sum[0].multiply(new BigDecimal(ratio));
    }
}
